﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace FRAFV.Binary.Serialization
{
	public class CString
	{
		public enum TextEncodings { UTF8, Macintosh, UTF16LE, UTF16BE, UTF32LE, UTF32BE }

		private static readonly byte[] EmptyData = new byte[0];
		private static readonly Encoding UTF8 = Encoding.UTF8;
		private static readonly Encoding MacEncoding = Encoding.GetEncoding("macintosh");
		private static readonly Encoding UTF16LE = Encoding.Unicode;
		private static readonly Encoding UTF16BE = Encoding.BigEndianUnicode;
		private static readonly Encoding UTF32LE = Encoding.UTF32;
		private static readonly Encoding UTF32BE = new UTF32Encoding(true, true);
		private static readonly ushort BOMUTF16LE = BitConverter.ToUInt16(UTF16LE.GetPreamble(), 0);
		private static readonly ushort BOMUTF16BE = BitConverter.ToUInt16(UTF16BE.GetPreamble(), 0);
		private static readonly int BOMUTF8 = ToUInt24(UTF8.GetPreamble());
		private static readonly uint BOMUTF32LE = BitConverter.ToUInt32(UTF32LE.GetPreamble(), 0);
		private static readonly uint BOMUTF32BE = BitConverter.ToUInt32(UTF32BE.GetPreamble(), 0);
		[XmlIgnore]
		public Encoding TextEncoding { get; private set; }
		[XmlAttribute("Encoding")]
		public TextEncodings CharSet
		{
			get {
				if (this.TextEncoding == UTF8)
					return TextEncodings.UTF8;
				if (this.TextEncoding == UTF16LE)
					return TextEncodings.UTF16LE;
				if (this.TextEncoding == UTF16BE)
					return TextEncodings.UTF16BE;
				if (this.TextEncoding == UTF32LE)
					return TextEncodings.UTF32LE;
				if (this.TextEncoding == UTF32BE)
					return TextEncodings.UTF32BE;
				return TextEncodings.Macintosh;
			}
			set {
				switch (value)
				{
				case TextEncodings.Macintosh:
					this.TextEncoding = MacEncoding;
					break;
				case TextEncodings.UTF8:
					this.TextEncoding = UTF8;
					break;
				case TextEncodings.UTF16BE:
					this.TextEncoding = UTF16BE;
					break;
				case TextEncodings.UTF16LE:
					this.TextEncoding = UTF16LE;
					break;
				case TextEncodings.UTF32BE:
					this.TextEncoding = UTF32BE;
					break;
				case TextEncodings.UTF32LE:
					this.TextEncoding = UTF32LE;
					break;
				}
			}
		}
		[XmlIgnore]
		public bool IsUTF16
		{
			get { return this.TextEncoding == UTF16LE || this.TextEncoding == UTF16BE; }
		}
		[XmlIgnore]
		public bool IsUTF32
		{
			get { return this.TextEncoding == UTF32LE || this.TextEncoding == UTF32BE; }
		}
		[XmlIgnore]
		public byte[] Data { get; private set; }
		[XmlText]
		public string Text
		{
			get { return this.TextEncoding.GetString(this.Data); }
			set { this.Data = this.TextEncoding.GetBytes(value); }
		}

		public CString()
		{
			this.TextEncoding = Encoding.UTF8;
			this.Data = EmptyData;
		}

		private static int ToUInt24(this byte[] num, int start = 0)
		{
			var buf = new byte[4];
			if (!BitConverter.IsLittleEndian)
			{
				buf[0] = 0; buf[1] = num[start + 2]; buf[2] = num[start + 1]; buf[3] = num[start];
				return BitConverter.ToInt32(buf, 0);
			}
			else
			{
				buf[0] = num[start]; buf[1] = num[start + 1]; buf[2] = num[start + 2]; buf[3] = 0;
				return BitConverter.ToInt32(buf, 0);
			}
		}



		public static explicit operator string(CString text)
		{
			return text.Text;
		}

		public override string ToString()
		{
			return this.Text;
		}

		private int FindEndOfString(byte[] data, bool last, out int offset)
		{
			offset = 0;
			if (this.TextEncoding != MacEncoding)
			{
				if (data.Length >= 2)
				{
					ushort bom16 = BitConverter.ToUInt16(data, 0);
					if (bom16 == BOMUTF16BE || bom16 == BOMUTF16LE)
					{
						offset = 2;
						this.TextEncoding = bom16 == BOMUTF16BE ? UTF16BE : UTF16LE;
						if (last) return data.Length;
						for (int k = 2; k < data.Length - 1; k += 2)
							if (data[k] == 0 && data[k + 1] == 0)
								return k;
					}
				}
				if (data.Length >= 3)
				{
					int bom24 = ToUInt24(data);
					if (bom24 == BOMUTF8)
						offset = 3;
				}
				if (data.Length >= 4)
				{
					uint bom32 = BitConverter.ToUInt32(data, 0);
					if (bom32 == BOMUTF32BE || bom32 == BOMUTF32LE)
					{
						offset = 4;
						this.TextEncoding = bom32 == BOMUTF32BE ? UTF32BE : UTF32LE;
						if (last) return data.Length;
						for (int k = 4; k < data.Length - 3; k += 4)
							if (data[k] == 0 && data[k + 1] == 0 && data[k + 2] == 0 && data[k + 3] == 0)
								return k;
					}
				}
			}
			int stop = Array.IndexOf(data, (byte)0, offset);
			return stop < 0 || last ? data.Length : stop;
		}

		private long Read(byte[] data, bool last = true)
		{
			int offset;
			int len = FindEndOfString(data, last, out offset);
			if (len == data.Length && offset == 0)
			{
				this.Data = data;
				return 0;
			}
			var data3 = new byte[len - offset];
			Array.Copy(data, offset, data3, 0, len - offset);
			this.Data = data3;
			return data.Length - len;
		}

		public CString(BinaryReader reader, ref long length, Encoding encoding)
		{
			this.TextEncoding = encoding;
			length = Read(reader.ReadBytes((int)length), false);
		}

		public CString(BinaryReader reader, long length, Encoding encoding)
		{
			this.TextEncoding = encoding;
			Read(reader.ReadBytes((int)length));
		}

		public byte[] Write(bool terminate = false)
		{
			if (!terminate && (this.TextEncoding == MacEncoding || this.TextEncoding == UTF8))
				return this.Data;
			var mem = new MemoryStream();
			var writer = new BinaryWriter(mem);
			if (this.TextEncoding == UTF16LE)
				writer.Write(BitConverter.GetBytes(BOMUTF16LE));
			else if (this.TextEncoding == UTF16BE)
				writer.Write(BitConverter.GetBytes(BOMUTF16BE));
			else if (this.TextEncoding == UTF32LE)
				writer.Write(BitConverter.GetBytes(BOMUTF32LE));
			else if (this.TextEncoding == UTF32BE)
				writer.Write(BitConverter.GetBytes(BOMUTF32BE));
			writer.Write(this.Data);
			if (terminate)
				if (this.IsUTF16)
					writer.Write((ushort)0);
				else if (this.IsUTF32)
					writer.Write((uint)0);
				else
					writer.Write((byte)0);
			return mem.ToArray();
		}

		public long DataSize(bool terminate = false)
		{
			return this.IsUTF16 ? 2 + this.Data.Length + (terminate ? 2 : 0) :
				this.IsUTF32 ? 4 + this.Data.Length + (terminate ? 4 : 0) :
				this.Data.Length + (terminate ? 1 : 0);
		}
	}
}
